package org.gienah.testing.junit;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gienah.testing.xml.TestDefinition;
import org.junit.internal.runners.CompositeRunner;
import org.junit.internal.runners.InitializationError;
import org.junit.internal.runners.TestClassMethodsRunner;
import org.junit.internal.runners.TestClassRunner;
import org.junit.internal.runners.TestMethodRunner;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

/**
 * A <code>SpringRunner</code> runs tests and provides access to defined
 * Spring related features like bean injection and transactional tests. To use
 * this runner, you need to mark your test class with the <code>RunWith</code>
 * annotation. Besides, you must to specify the Spring configuration using the
 * <code>Configuration</code> annotation.
 * 
 * @see org.gienah.testing.junit.Configuration
 * @see org.junit.internal.runners.TestClassRunner
 * @see org.junit.runner.Runner
 * @see org.junit.runner.RunWith
 * @author Santiago L. Valdarrama
 * @since E0.1
 */
public class SpringRunner
		extends TestClassRunner {

	private static final Log logger = LogFactory.getLog(SpringRunner.class);

	@Dependency
	private PlatformTransactionManager transactionManager;
	private TransactionStatus transactionStatus;

	/**
	 * Constructs a new instance of <code>SpringRunner</code> class.
	 * 
	 * @param clazz The class containing the tests to be run.
	 * @throws InitializationError Thrown if anything wrong happen during runner
	 *         construction.
	 */
	public SpringRunner(Class<?> clazz)
			throws InitializationError {

		super(clazz, new SpringCompositeRunner(clazz));

	}

	public void initializeTransaction() {
		if (this.transactionManager != null) {
			if (this.transactionStatus == null) {
				this.transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
				if (SpringRunner.logger.isDebugEnabled()) {
					SpringRunner.logger.debug("Began transaction. Transaction manager [" + this.transactionManager + "]");
				}
			}
			else {
				SpringRunner.logger.warn("Thereis an existing unclosed transaction. Tests will not run within a transaction.");
			}
		}
		else {
			SpringRunner.logger.warn("The transaction manager is not set. Tests will not run within a transaction.");
		}
	}

	public void finalizeTransaction() {
		if (this.transactionStatus != null && !this.transactionStatus.isCompleted()) {
			try {
				this.transactionManager.rollback(this.transactionStatus);
				if (SpringRunner.logger.isDebugEnabled()) {
					SpringRunner.logger.debug("Rolled back transaction after test execution");
				}
			}
			finally {
				this.transactionStatus = null;
			}
		}
	}

	/**
	 * This method is responsible of injecting defined dependencies and running
	 * the tests for this runner.
	 * 
	 * @param notifier will be notified of events while tests are being run,
	 *        tests being started, finishing, and failing
	 */
	@Override
	public void run(RunNotifier notifier) {
		try {
			injectDependencies(notifier);
		}
		catch (Throwable t) {
			notifier.fireTestFailure(new Failure(getDescription(), t));
		}

		((SpringCompositeRunner) super.fEnclosedRunner).intializeRunner(this);

		super.run(notifier);
	}

	private void injectDependencies(RunNotifier notifier)
			throws Exception {

		Class<?> currentClass = getClass();
		do {
			try {
				Injector.inject(CacheProvider.getRunnerDefinition(this.getClass(), getTestClass()),
						this,
						currentClass,
						CacheProvider.getContext(getTestClass()));
			}
			catch (Throwable t) {
				notifier.fireTestFailure(new Failure(getDescription(), t));
			}

			currentClass = currentClass.getSuperclass();
		}
		while (!currentClass.equals(SpringRunner.class.getSuperclass()));

		Injector.inject(CacheProvider.getTestDefinition(getTestClass()), null, getTestClass(), CacheProvider.getContext(getTestClass()));
	}

	private static class SpringCompositeRunner
			extends CompositeRunner {

		private SpringTestClassMethodsRunner testClassMethodsRunner;

		public SpringCompositeRunner(Class<?> clazz)
				throws InitializationError {

			super(clazz.getName());

			this.testClassMethodsRunner = new SpringTestClassMethodsRunner(clazz);
			super.add(this.testClassMethodsRunner);
		}

		public void intializeRunner(SpringRunner runner) {
			this.testClassMethodsRunner.intializeRunner(runner);
		}

	}

	private static class SpringTestClassMethodsRunner
			extends TestClassMethodsRunner {

		private TestDefinition testDefinition;
		private SpringRunner runner;

		public SpringTestClassMethodsRunner(Class<?> clazz)
				throws InitializationError {

			super(clazz);

			try {
				this.testDefinition = CacheProvider.getTestDefinition(getTestClass());
			}
			catch (ConfigurationException e) {
				throw new InitializationError(e);
			}
		}

		public void intializeRunner(SpringRunner runner) {
			this.runner = runner;
		}

		@Override
		protected Object createTest()
				throws Exception {

			Object test = super.createTest();
			Injector.inject(this.testDefinition, test, test.getClass(), CacheProvider.getContext(test.getClass()));

			return test;
		}

		@Override
		protected TestMethodRunner createMethodRunner(Object test, Method method, RunNotifier notifier) {
			boolean ignore = this.testDefinition != null && this.testDefinition.isIgnoredMethod(method.getName());

			boolean transactional = false;
			if (!ignore) {
				transactional = getTestClass().getAnnotation(Transactional.class) != null;
				if (!transactional) {
					transactional = method.getAnnotation(Transactional.class) != null;
					if (!transactional) {
						transactional = this.testDefinition != null && this.testDefinition.isTransactionalMethod(method.getName());
					}
				}
			}

			return new SpringTestMethodRunner(test, method, notifier, methodDescription(method), ignore, transactional);
		}

		private class SpringTestMethodRunner
				extends TestMethodRunner {

			private final RunNotifier notifier;
			private final boolean ignore;
			private final boolean transactional;

			public SpringTestMethodRunner(Object test,
					Method method,
					RunNotifier notifier,
					Description description,
					boolean ignore,
					boolean transactional) {

				super(test, method, notifier, description);
				this.notifier = notifier;
				this.ignore = ignore;
				this.transactional = transactional;
			}

			@Override
			public void run() {
				if (this.ignore) {
					this.notifier.fireTestIgnored(getDescription());
					return;
				}

				super.run();
			}

			@Override
			public void runProtected() {
				if (this.transactional) {
					SpringTestClassMethodsRunner.this.runner.initializeTransaction();
				}

				try {
					super.runProtected();
				}
				finally {
					if (this.transactional) {
						SpringTestClassMethodsRunner.this.runner.finalizeTransaction();
					}
				}
			}
		}
	}

}
